package eztools

import (
	"bytes"
	"crypto/tls"
	"encoding/json"
	"io"
	"mime/multipart"
	"net/http"
	"os"
	"path/filepath"
	"strconv"
	"strings"
	"time"

	"github.com/mongodb-forks/digest"
)

const (
	AUTH_NONE = iota
	AUTH_PLAIN
	AUTH_BASIC
	AUTH_DIGEST
	METHOD_GET     = "GET"
	METHOD_PUT     = "PUT"
	METHOD_POST    = "POST"
	METHOD_DEL     = "DELETE"
	BODY_TYPE_JSON = "json"
	BODY_TYPE_FILE = "file"
	BODY_TYPE_TEXT = "text"
)

var AUTH_INSECURE_TLS bool

type AuthInfo struct {
	Type       int
	User, Pass string
}

func genFile(defRdr io.Reader, fType, fName string) (string, io.Reader) {
	if len(fName) < 1 {
		if len(fType) < 1 {
			return "application/json; charset=utf-8", defRdr
		}
		return fType, defRdr
	}
	if len(fType) < 1 {
		fType = "file"
	}
	var err error
	defer func() {
		if err != nil {
			LogPrint(err)
		}
	}()
	// New multipart writer.
	body := &bytes.Buffer{}
	writer := multipart.NewWriter(body)
	fw, err := writer.CreateFormFile(fType, filepath.Base(fName))
	if err != nil {
		return fType, defRdr
	}
	file, err := os.Open(fName)
	if err != nil {
		return fType, defRdr
	}
	_, err = io.Copy(fw, file)
	if err != nil {
		return fType, defRdr
	}
	writer.Close()
	return writer.FormDataContentType(), bytes.NewReader(body.Bytes())
}

func genReq(method, url string, bodyReq io.Reader, fType, fName string,
	hdrs map[string]string) (req *http.Request, err error) {
	bodyType, bodyReq := genFile(bodyReq, fType, fName)
	req, err = http.NewRequest(method, url, bodyReq)
	if err != nil {
		if Debugging {
			ShowStrln("failed to create " + method)
		}
		return
	}
	addHdr := func(nm, vl string) {
		_, ok := hdrs[nm]
		if ok {
			return
		}
		req.Header.Add(nm, vl)
	}
	if bodyReq != nil {
		addHdr("Content-Type", bodyType)
		/*if Debugging && Verbose > 2 {
			ShowStrln("body type=" + bodyType)
		}*/
	}
	addHdr("Accept", "*/*")
	for n, v := range hdrs {
		req.Header.Add(n, v)
		/*if Debugging && Verbose > 2 {
			ShowStrln("adding header:" + n + "=" + v)
		}*/
	}
	return
}

// RestGetBody get body from response, stripping magic.
// Return values: bodyType and statusCode are returned as long as resp is provided.
//
//	bodyBytes is nil if Content-Length is 0 in header.
//	ErrInvalidInput=no response input
//	ErrOutOfBound=magic not matched
//	ErrNoValidResults=bad status code (non 2xx)
//	other errors from io.ReadAll()
func RestGetBody(resp *http.Response, magic []byte) (bodyType string,
	bodyBytes []byte, statusCode int, err error) {
	if resp == nil {
		err = ErrInvalidInput
		return
	}
	defer resp.Body.Close()

	bodyType = resp.Header.Get("Content-Type")
	statusCode = resp.StatusCode
	bodyBytes, err = io.ReadAll(resp.Body)
	if statusCode < http.StatusOK || statusCode >= http.StatusBadRequest {
		if Debugging && Verbose > 1 {
			ShowStrln("failure response " + strconv.Itoa(statusCode))
		}
		err = ErrNoValidResults
		/*var b []byte
		if resp.ContentLength > 0 {
			b = make([]byte, resp.ContentLength)
			resp.Body.Read(b)
		}
		return "", b, statusCode, errors.New(resp.Status)*/
		return
	}
	if err != nil { // body not read
		//LogErrPrint(err)
		return
	}

	/*if Debugging && Verbose > 2 {
		ShowStrln("resp code=" + strconv.Itoa(statusCode))
	}*/
	if cl := resp.Header.Get("Content-Length"); cl == "0" {
		/*if Debugging && Verbose > 2 {
			ShowStrln("no body in response")
		}*/
		return
	}
	if bodyBytes == nil || len(bodyBytes) < 1 {
		/*if Debugging && Verbose > 2 {
			LogPrintWtTime("no body")
		}*/
		return
	}
	if len(magic) > 0 {
		/*if Debugging && Verbose > 1 {
			ShowStrln("stripping magic")
		}*/
		if bytes.HasPrefix(bodyBytes, magic) {
			bodyBytes = bytes.TrimLeft(bytes.TrimPrefix(bodyBytes,
				magic), "\n\r")
		} else {
			err = ErrOutOfBound
			return
		}
	}
	/*if Debugging && Verbose > 2 {
		LogPrintWtTime("type", bodyType, "body", bodyBytes)
	}*/
	return
}

// RestParseBody parses body from response to json or file.
//
//	text body is not processed.
//
// Return values: ErrIncomplete=not parsed because type not recognized
//
//	other errors are from RestGetBody(), json.Unmarshal() or FileWrite()
func RestParseBody(resp *http.Response, fileName string, strucOut interface{},
	magic []byte) (recognized, bodyType string, bodyBytes []byte, statusCode int, err error) {
	bodyType, bodyBytes, statusCode, err = RestGetBody(resp, magic)
	if err != nil || (bodyBytes == nil || len(bodyBytes) < 1) {
		return
	}
	switch {
	case strings.Contains(bodyType, "application/json"):
		recognized = BODY_TYPE_JSON
		if strucOut != nil {
			err = json.Unmarshal(bodyBytes, strucOut)
		}
	case strings.Contains(bodyType, "text/plain"),
		strings.Contains(bodyType, "application/xml"),
		strings.Contains(bodyType, "application/xhtml+xml"),
		strings.Contains(bodyType, "application/javascript"),
		strings.Contains(bodyType, "text/javascript"):
		recognized = BODY_TYPE_TEXT
		// bytes are maybe better than string, so leave them as are
		/*if Debugging && Verbose > 2 {
			LogPrintWtTime("type", bodyType, "body", bodyBytes)
		}*/
	case strings.Contains(bodyType, "file"),
		strings.Contains(bodyType, "audio"),
		strings.Contains(bodyType, "video"),
		strings.Contains(bodyType, "application"),
		strings.Contains(bodyType, "image"):
		recognized = BODY_TYPE_FILE
		if len(fileName) > 0 {
			err = FileWrite(fileName, bodyBytes)
			/*var out *os.File
			out, err = os.Create(fileName)
			if err != nil {
				break
			}
			defer out.Close()
			_, err = io.Copy(out, bytes.NewReader(bodyBytes))*/
		}
	default:
		err = ErrIncomplete
	}
	return
}

func restSend(method, url string, to time.Duration,
	bodyReq io.Reader, fType, fName string, hdrs map[string]string,
	funcReqProc func(req *http.Request),
	funcReqSend func(req *http.Request) (*http.Response,
		error)) (resp *http.Response, err error) {
	/*if Debugging && Verbose > 1 {
		ShowStrln(method + " " + url)
	}*/
	req, err := genReq(method, url, bodyReq, fType, fName, hdrs)
	if err != nil {
		return
	}
	if funcReqProc != nil {
		funcReqProc(req)
	}
	if Debugging && Verbose > 2 {
		LogWtTime(*req)
	}
	//ShowSthln(req.ContentLength)
	if funcReqSend == nil {
		tc := &tls.Config{InsecureSkipVerify: AUTH_INSECURE_TLS}
		tr := &http.Transport{TLSClientConfig: tc}
		cli := &http.Client{Timeout: to, Transport: tr}
		funcReqSend = cli.Do
	}
	resp, err = funcReqSend(req)
	if err != nil {
		if Debugging {
			ShowStrln("failed to send/get")
		}
		/*} else {
		if Debugging && Verbose > 2 {
			LogWtTime(*resp)
		}*/
	}
	return
}

const defRestGetTO = 60 * time.Second

func restSendNParseResp(method, url string, authInfo AuthInfo,
	bodyReq io.Reader, fType, fileOut string,
	hdrs map[string]string) (resp *http.Response, err error) {
	switch authInfo.Type {
	case AUTH_DIGEST:
		resp, err = restSend(method, url, 0, bodyReq,
			fType, fileOut, hdrs, nil,
			func(req *http.Request) (*http.Response, error) {
				t := digest.NewTransport(authInfo.User, authInfo.Pass)
				return t.RoundTrip(req)
			})
	case AUTH_BASIC:
		resp, err = restSend(method, url, defRestGetTO, bodyReq,
			fType, fileOut, hdrs,
			func(req *http.Request) {
				if len(authInfo.Pass) > 0 {
					req.SetBasicAuth(authInfo.User, authInfo.Pass)
				}
			}, nil)
	case AUTH_PLAIN:
		resp, err = restSend(method, url, defRestGetTO, bodyReq,
			fType, fileOut, hdrs,
			func(req *http.Request) {
				if len(authInfo.Pass) > 0 {
					req.Header.Set("authorization", "Basic "+authInfo.Pass)
				}
			}, nil)
	default: // AUTH_NONE
		resp, err = restSend(method, url, defRestGetTO, bodyReq,
			fType, fileOut, hdrs, nil, nil)
	}
	//statusCode = resp.StatusCode
	return
}

/*
	RestSendFileNHdr sends Restful API and returns the result.

Specify a file with name and type to be sent as body, and/or extra headers.
If something wrong with the file, a request will be sent without it anyway.
*/
func RestSendFileNHdr(method, url string, authInfo AuthInfo,
	fType, fName string, hdrs map[string]string) (*http.Response, error) {
	return restSendNParseResp(method,
		url, authInfo, nil, fType, fName, hdrs)
}

// RestSendHdr sends Restful API and returns the result.
// With body and/or extra headers.
func RestSendHdr(method, url string, authInfo AuthInfo,
	bodyReq io.Reader, hdrs map[string]string) (*http.Response, error) {
	return restSendNParseResp(method,
		url, authInfo, bodyReq, "", "", hdrs)
}

// RestSend sends Restful API request and returns the result.
// = HttpSend + AuthInfo
func RestSend(method, url string, authInfo AuthInfo,
	bodyReq io.Reader) (*http.Response, error) {
	return restSendNParseResp(method,
		url, authInfo, bodyReq, "", "", nil)
}

// HttpSend sends HTTP request and returns the result.
// It does not need AuthInfo as RestSend.
// Set AUTH_INSECURE_TLS to true to skip TLS (X509) verification
func HttpSend(method, url string, bodyReq io.Reader) (*http.Response, error) {
	return restSendNParseResp(method, url,
		AuthInfo{}, bodyReq, "", "", nil)
}

/*
	RangeStrMap iterate through map[string]interface{} obj, calling fun for

each element recursively. When fun returns true, it stops.
false is returned if no element found.
*/
func RangeStrMap(obj interface{}, fun func(k string, v interface{}) bool) bool {
	//if the argument is not a map, ignore it
	mobj, ok := obj.(map[string]interface{})
	if !ok {
		return false
	}

	for k, v := range mobj {
		//key match, return value
		if fun(k, v) {
			return true
		}

		//if the value is a map, search recursively
		if m, ok := v.(map[string]interface{}); ok {
			if RangeStrMap(m, fun) {
				return true
			}
		}
		//if the value is an array, search recursively
		//from each element
		if va, ok := v.([]interface{}); ok {
			for _, a := range va {
				if RangeStrMap(a, fun) {
					return true
				}
			}
		}
	}

	//element not found
	return false
}

/*
	FindStrMap find string key in map[string]interface{} obj,

returning the value and true or nil and false.
*/
func FindStrMap(obj interface{}, key string) (interface{}, bool) {
	//if the argument is not a map, ignore it
	mobj, ok := obj.(map[string]interface{})
	if !ok {
		return nil, false
	}

	for k, v := range mobj {
		//key match, return value
		if k == key {
			return v, true
		}

		//if the value is a map, search recursively
		if m, ok := v.(map[string]interface{}); ok {
			if res, ok := FindStrMap(m, key); ok {
				return res, true
			}
		}
		//if the value is an array, search recursively
		//from each element
		if va, ok := v.([]interface{}); ok {
			for _, a := range va {
				if res, ok := FindStrMap(a, key); ok {
					return res, true
				}
			}
		}
	}

	//element not found
	return nil, false
}
